package com.qsl.utils;

import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.CompressionMethod;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.apache.commons.compress.utils.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;

/**
 * Description: 文件压缩与解压工具类
 */
@Slf4j
public class FileCompressDecompressUtil {

    public static final String ZIP_TYPE = ".zip";

    public static final String GZ_TYPE = ".gz";

    public static final String TAR_GZ_TYPE = ".tar.gz";

    public static boolean isCompressFile(String fileName) {
        if (StringUtils.isBlank(fileName)) {
            return false;
        }
        if (fileName.endsWith(ZIP_TYPE)) {
            return true;
        }
        if (fileName.endsWith(GZ_TYPE)) {
            return true;
        }
        return fileName.endsWith(TAR_GZ_TYPE);
    }

    public static List<File> decompress(File sourceFile, String destPath) throws IOException {
        // 创建父目录
        Path parentDir = Paths.get(destPath);
        if (Files.notExists(parentDir)) {
            Files.createDirectories(parentDir);
        }
        return decompress(sourceFile, destPath, getCompressType(sourceFile.getName()));
    }

    public static List<File> decompress(File sourceFile, String destPath, String compressType) throws IOException {
        switch (compressType) {
            case ZIP_TYPE:
                return decompressZipFile(sourceFile, destPath);
            case GZ_TYPE:
                return decompressGzipFile(sourceFile, destPath);
            case TAR_GZ_TYPE:
                return decompressTarGzipFile(sourceFile, destPath);
            default:
                throw new IllegalArgumentException("不支持的解压类型：" + compressType);
        }
    }

    public static List<File> decompressZipFile(File sourceFile, String destPath) throws IOException {
        List<File> files = new ArrayList<>();
        try(ZipFile zipFile = new ZipFile(sourceFile)) {
            zipFile.extractAll(destPath);
            List<FileHeader> fileHeaders = zipFile.getFileHeaders();
            for (FileHeader fileHeader : fileHeaders) {
                files.add(new File(destPath + File.separator + fileHeader.getFileName()));
            }
        }
        return files;
    }

    public static List<File> decompressGzipFile(File sourceFile, String destPath) throws IOException {
        String fileName = sourceFile.getName();

        byte[] buf = new byte[1024];
        String outFileName = destPath + File.separator + fileName.substring(0, fileName.lastIndexOf('.'));
        try (FileInputStream fis = new FileInputStream(sourceFile);
             BufferedInputStream bis = new BufferedInputStream(fis);
             GZIPInputStream zis = new GZIPInputStream (bis);
             FileOutputStream out = new FileOutputStream(outFileName)){
            int len;
            while((len = zis.read(buf)) > 0) {
                out.write(buf, 0, len);
            }
            out.flush();
        }
        return Lists.newArrayList(new File(outFileName));
    }

    public static List<File> decompressTarGzipFile(File sourceFile, String destPath) throws IOException {
        // 建立输出流，用于将从压缩文件中读出的文件流写入到指定目录下
        List<File> files = new ArrayList<>();
        TarArchiveEntry entry = null;
        try (FileInputStream fis = new FileInputStream(sourceFile);
             GZIPInputStream gis = new GZIPInputStream(fis);
             TarArchiveInputStream taris = new TarArchiveInputStream(gis);) {
            while ((entry = taris.getNextTarEntry()) != null) {
                File entryFile = new File(destPath + File.separator + entry.getName());
                OutputStream out = Files.newOutputStream(entryFile.toPath());
                IOUtils.copy(taris, out);
                files.add(entryFile);
                out.close();
            }
        }
        return files;
    }

    /**
     * 压缩，压缩包中的文件修改时间会保留原来的
     * @param destFilePath 压缩文件路径
     * @param sources 源文件列表
     * @throws IOException 压缩异常
     */
    public static void compress(String destFilePath, List<File> sources) throws IOException {
        String compressType = getCompressType(destFilePath);
        switch (compressType) {
            case ZIP_TYPE:
                compressZip(destFilePath, sources);
                break;
            case GZ_TYPE:
                compressGzip(destFilePath, sources);
                break;
            case TAR_GZ_TYPE:
                compressTarGzip(destFilePath, sources);
                break;
            default:
                throw new IllegalArgumentException("不支持的压缩类型：" + compressType);
        }
    }

    /**
     * zip 压缩
     *      压缩包中的文件修改时间会保留原来的
     * @param destFilePath 压缩文件路径
     * @param sources 源文件列表
     * @throws IOException 压缩异常
     */
    public static void compressZip(String destFilePath, List<File> sources) throws IOException {
        try(ZipFile zipFile = new ZipFile(destFilePath)) {
            for (File sourceFile : sources) {
                ZipParameters param = new ZipParameters();
                param.setCompressionMethod(CompressionMethod.DEFLATE);
                param.setCompressionLevel(CompressionLevel.NORMAL);
                param.setLastModifiedFileTime(sourceFile.lastModified());
                param.setFileNameInZip(sourceFile.getName());
                try (FileInputStream is = new FileInputStream(sourceFile)) {
                    zipFile.addStream(is, param);
                }
            }
        }
    }

    /**
     * gz 压缩
     *      压缩包中的文件修改时间会保留原来的
     * @param destFilePath 压缩文件路径
     * @param sources 源文件列表
     * @throws IOException 压缩异常
     */
    public static void compressGzip(String destFilePath, List<File> sources) throws IOException {
        if (sources.size() > 1) {
            throw new IllegalArgumentException("gz不支持多个文件压缩");
        }
        File sourceFile = sources.get(0);
        GzipParameters parameters = new GzipParameters();
        parameters.setFilename(sourceFile.getName());
        parameters.setModificationTime(sourceFile.lastModified());
        try (FileOutputStream fos = new FileOutputStream(destFilePath);
             GzipCompressorOutputStream gos = new GzipCompressorOutputStream(fos, parameters);
             InputStream is = Files.newInputStream(sourceFile.toPath())){
            IOUtils.copy(is, gos);
        }
    }

    /**
     * tar.gz 压缩
     *      压缩包中的文件修改时间会保留原来的
     * @param destFilePath 压缩文件路径
     * @param sources 源文件列表
     * @throws IOException 压缩异常
     */
    public static void compressTarGzip(String destFilePath, List<File> sources) throws IOException {

        try(GzipCompressorOutputStream gos = new GzipCompressorOutputStream(Files.newOutputStream(Paths.get(destFilePath)));
            TarArchiveOutputStream tos = new TarArchiveOutputStream(gos);
            WritableByteChannel writeChannel = Channels.newChannel(tos)) {
            tos.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
            for (File sourceFile : sources) {
                try (FileInputStream is = new FileInputStream(sourceFile);
                     FileChannel readChannel = is.getChannel()) {
                    TarArchiveEntry tarArchiveEntry = new TarArchiveEntry(sourceFile, sourceFile.getName());
                    tarArchiveEntry.setLastModifiedTime(FileTime.fromMillis(sourceFile.lastModified()));
                    tos.putArchiveEntry(tarArchiveEntry);
                    readChannel.transferTo(0, sourceFile.length(), writeChannel);
                    tos.closeArchiveEntry();
                }
            }
        }
    }

    /**
     * 递归删除目录
     * @param destDir 目标目录
     */
    public static void recursiveDelDir(String destDir) throws IOException {
        Path unzipDir = Paths.get(destDir);
        if (Files.notExists(unzipDir)) {
            return;
        }
        try (Stream<Path> walk = Files.walk(unzipDir)) {
            walk.sorted(Comparator.reverseOrder()).forEach(path -> {
                try {
                    Files.delete(path);
                } catch (IOException e) {
                    log.warn("[{}]删除失败", path, e);
                }
            });
        }
    }

    private static String getCompressType(String fileName) {
        if (fileName.endsWith(TAR_GZ_TYPE)) {
            return TAR_GZ_TYPE;
        } else if (fileName.endsWith(GZ_TYPE)) {
            return GZ_TYPE;
        } else if (fileName.endsWith(ZIP_TYPE)) {
            return ZIP_TYPE;
        } else {
            return fileName.substring(fileName.lastIndexOf('.'));
        }
    }
}
